package com.zkh.myframe.server.tomcat;

import java.io.File;
import java.io.IOException;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Optional;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Stream;

import com.zkh.myframe.server.ServerConfig;
import com.zkh.myframe.server.tomcat.annotation.Server;
import com.zkh.myframe.server.tomcat.annotation.ServerType;
import com.zkh.myutils.code.Charsets;
import com.zkh.myutils.io.IOUtils;
import com.zkh.myutils.io.file.FileUtils;
import com.zkh.myutils.io.file.FileWriter;
import com.zkh.myutils.system.ConfigLoader;
import com.zkh.myutils.utils.Regexp;
import com.zkh.myutils.utils.StringUtils;
import com.zkh.myutils.utils.UtilsException;

/**
 * tomcaat项目发布工具。主要功能有两个：<br>
 * 1、本地启动调试。<br>
 * 2、打jar包，正式发布。
 * @author zkh
 */
public class TomcatPublish {
	/**
	 *项目路径：myframe/
	 */
	private static File root = new File(System.getProperty("user.dir"));
	/**
	 * 需要过滤的jar包
	 */
	private static List<String> jarFilterList = Arrays.asList("idea_rt.jar", "javaagent-shaded.jar");
	
	//需要从myframe包抽取的class文件的正则
	private static String extractClassPattern = "com/zkh/myframe/server/JarInJarClassLoader\\.class";
	//加载器类
	private static String loaderClass = "com.zkh.myframe.server.JarInJarClassLoader";
	//时间格式
	private static SimpleDateFormat sdf = new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss]");
	
	/**
	 * 执行发布
	 * @param clazz 运行类Class，需要读取注解信息
	 */
	public static void run(Class<?> clazz) throws IOException {
		run(clazz, null);
	}
	
	/**
	 * 执行发布，参数路径均基于项目根目录。
	 * @param clazz 运行类Class，需要读取注解信息
	 * @param activeProfileModule 生成的启动脚本需要激活的配置文件模块，覆盖profile配置项的值
	 */
	public static void run(Class<?> clazz, String activeProfileModule) throws IOException {
		//日志
		printLog("开始检查运行类Class。。。");
		//运行类Class
		if(clazz==null) {
			throw new UtilsException("运行类Class不能为空");
		}
		//日志
		printLog("开始检查Server注解。。。");
		//获取Server注解
		Server server = clazz.getAnnotation(Server.class);
		if(server==null) {
			throw new NullPointerException("请添加@Server注解");
		}
		//初始化加载配置文件（.my），如果profile不为null，将额外加载配置文件。如profile为prod，则加载（-prod.my）
		ConfigLoader.initialize(activeProfileModule);
		//模块名
		String moduleName = ConfigLoader.getEntity("server", ServerConfig.class).getModuleName(server.moduleName());
		//有模块名
		if(StringUtils.isNotEmpty(moduleName)){
			root = new File(root, moduleName);
		}
		//项目名称
		String projectName = root.getName();
		//日志
		printLog("开始检查编译路径。。。");
		//编译路径
		File compileFile = new File(URLDecoder.decode(clazz.getProtectionDomain().getCodeSource().getLocation().getPath(), "UTF-8"));
		//日志
		printLog("开始创建临时目录用来存放项目文件。。。");
		//目标路径
		File targetRoot = new File(compileFile, projectName);
		//如果目录存在，则先清空，否则创建目录
		if(targetRoot.isDirectory()) {
			FileUtils.deleteChildren(targetRoot);
		}else {
			targetRoot.mkdir();
		}
		//目标临时打包路径
		File targetPack = FileUtils.mkdirs( new File(targetRoot, "temp"));
		//日志
		printLog("开始拷贝编译文件。。。");
		//将编译文件复制到临时目录
		Optional.ofNullable(compileFile.list()).ifPresent(list-> Stream.of(list)
				//过滤创建的project目录
				.filter(f->!StringUtils.isExist(f, projectName, projectName+".zip"))
				//复制目录
				.forEach(f-> FileUtils.copy(new File(compileFile, f), targetPack)));
		//日志
		printLog("开始拷贝jar包文件。。。");
		//将依赖的jar打包到lib目录下
		File[] corePacks = copyLib(FileUtils.mkdirs(new File(targetPack, "lib")), projectName);
		//检查核心包
		if(corePacks[0]==null || corePacks[1]==null) {
			throw new RuntimeException("核心包myframe.jar或myutils.jar不存在");
		}
		//日志
		printLog("开始拷贝核心运行类。。。");
		//将匹配到的代码发布到jar包中
		pubCoreAnnoClass(targetPack, corePacks);
		//日志
		printLog("开始生成MANIFEST.MF文件。。。");
		//创建META-INF目录
		File metaInf = FileUtils.mkdirs(new File(targetPack, "META-INF"));
		//创建MANIFEST.MF文件
		File manifest = FileUtils.createFile(new File(metaInf, "MANIFEST.MF"));
		//写入内容
		FileUtils.writeContent(manifest, getManifestInfo(clazz.getName()));
		/** 生成可执行的jar包 **/
		printLog("正在生成" + projectName + ".jar文件。。。");
		//打包
		FileUtils.compressChildrenToZip(targetPack.getAbsolutePath(), targetRoot.getAbsolutePath(), projectName+".jar");
		//删除临时目录temp
		FileUtils.delete(targetPack);
		
		/** 生成运行脚本 **/
		printLog("正在生成运行脚本。。。");
		createScript(targetRoot, projectName, activeProfileModule);
		
		//如果是JSP_VIEW项目，处理WebRoot下的文件
		if(server.type()==ServerType.JSP_VIEW) {
			//WebRoot目录
			File webRoot = new File(root, server.webRoot());
			//存放位置
			File targetWebRoot = FileUtils.mkdirs(new File(targetRoot, server.webRoot()));
			//复制文件
			FileUtils.copyChildren(webRoot, targetWebRoot, (filename)->!filename.isDirectory() || !"classes".equals(filename.getName()));
		}
		
		/** 打包**/
		printLog("开始压缩"+targetRoot.getAbsolutePath()+"目录为zip包。。。");
		//开始打包
		FileUtils.compressToZip(targetRoot.getAbsolutePath(), targetRoot.getParentFile().getPath());
		printLog("打包成功，正在删除"+targetRoot.getAbsolutePath()+"目录。。。");
		FileUtils.delete(targetRoot);
		
		printLog("项目发布成功!!!  :)");
	}
	
	/**
	 * 生成运行脚本和停用脚本
	 * @param dir 脚本所在目录
	 * @param projectName 项目名称
	 * @param activeProfileModule 生成的启动脚本需要激活的配置文件模块，覆盖profile配置项的值
	 */
	private static void createScript(File dir, String projectName, String activeProfileModule) {
		//激活模块
		String activeModule = StringUtils.isEmpty(activeProfileModule) ? "" : (" " + activeProfileModule);
		// windows脚本
		new FileWriter(new File(dir, "run.bat"), Charsets.GBK)
			.write("@echo off\r\n"
					+ "set /p order=请选择你的操作（1：启动应用，2：停用应用）：\r\n"
					+ "@REM pid文件\r\n"
					+ "set \"pidfile=%CD%\\pid\"\r\n"
					+ "if exist %pidfile% for /f %%i in (%pidfile%) do (set cpid=%%i)\r\n"
					+ "if \"%order%\"==\"1\" (\r\n"
					+ "	@REM 启动前先执行一次停止应用\r\n"
					+ "	if not \"%cpid%\"==\"\" (\r\n"
					+ "		taskkill>nul 2>nul -pid %cpid% -f\r\n"
					+ "	)\r\n"
					+ "	start javaw -jar " + projectName + ".jar" + activeModule +"\r\n"
					+ "	echo 操作完成\r\n"
					+ ") else if \"%order%\"==\"2\" (\r\n"
					+ "	if not \"%cpid%\"==\"\" (\r\n"
					+ "		taskkill>nul 2>nul -pid %cpid% -f\r\n"
					+ "		echo 应用已停止\r\n"
					+ "	) else echo 未从pid文件中读取到pid信息\r\n"
					+ ") else if \"%order%\"==\"\" (\r\n"
					+ "	echo 未输入操作命令\r\n"
					+ ") else (\r\n"
					+ "	echo 未知的操作：%order%\r\n"
					+ ")\r\n"
					+ "pause", true);
		// Linux脚本
		new FileWriter(new File(dir, "run.sh"))
			.write("#!/bin/bash\n"
					+ "\n"
					+ "if [ \"$1\" == \"start\" ]\n"
					+ "        then\n"
					+ "                # 查看是否有pid文件，如果有，先停用一次\n"
					+ "                if [ -r pid ]\n"
					+ "                        then\n"
					+ "                                " + projectName + "pid=$(head -n 1 pid)\n"
					+ "                                # 停用并屏蔽错误信息\n"
					+ "                                kill -9 $" + projectName + "pid 2>/dev/null\n"
					+ "                fi\n"
					+ "                #启动应用\n"
					+ "                nohup java -jar " + projectName + ".jar" + activeModule + " >/dev/null 2>&1 &\n"
					+ "                echo \"操作完成\"\n"
					+ "elif [ \"$1\" == \"stop\" ]\n"
					+ "        then\n"
					+ "                if [ -r pid ]\n"
					+ "                        then\n"
					+ "                                # 根据PID杀掉进程\n"
					+ "                                " + projectName + "pid=$(head -n 1 pid)\n"
					+ "                                kill -9 $" + projectName + "pid\n"
					+ "                                echo \"应用已停止\"\n"
					+ "                else\n"
					+ "                        echo \"未找到pid文件或文件不可读，命令执行失败\"\n"
					+ "                fi\n"
					+ "elif [ -z \"$1\" ]\n"
					+ "        then\n"
					+ "                echo \"请传递参数\"\n"
					+ "else\n"
					+ "        echo \"参数错误，必须为start或stop\"\n"
					+ "fi\n", true);
	}

	/**
	 * 将jar包中的class文件提取到发布目录
	 */
	private static void pubCoreAnnoClass(File classes, File[] corePacks) throws IOException {
		//读取jar包
		JarFile jarFile = new JarFile(corePacks[1]);
		//迭代器
		Enumeration<JarEntry> enumeration = jarFile.entries();
		//遍历
		while(enumeration.hasMoreElements()) {
			//当前目录或文件
			JarEntry entry = enumeration.nextElement();
			//名称
			String nm = entry.getName();
			//匹配模式
			if(nm.matches(extractClassPattern)) {
				//判断有无目录
				int index = nm.lastIndexOf("/");
				//目标文件
				File file;
				//等于-1
				if(index==-1) {
					//创建文件
					file = FileUtils.createFile(new File(classes, nm));
				}else {
					//先创建目录
					File dir = FileUtils.mkdirs(new File(classes, nm.substring(0, index)));
					//文件
					file = FileUtils.createFile(new File(dir, nm.substring(index+1)));
				}
				//写入内容
				FileUtils.saveInputStreamTo(jarFile.getInputStream(entry), file);
			}
		}
		//关闭包
		IOUtils.closeQuietly(jarFile);
	}

	/**
	 * 移动lib库，并返回目标myframe包和myutils包对象
	 * @param targetLib 目标lib目录
	 * @param projectName 项目名称   
	 */
	private static File[] copyLib(File targetLib, String projectName) {
		//核心包标识
		File[] corePacks = new File[2];
		//获取所有依赖
		for(String dep: System.getProperty("java.class.path").replace('\\', '/').split(";")){
			//创建File对象，用来检测类型
			File file = new File(dep);
			//转小写
			String lower = dep.toLowerCase();
			//文件名
			String filename = dep.substring(dep.lastIndexOf("/") + 1);
			//文件
			if(file.isFile()){
				//文件扩展名
				String ext = lower.substring(lower.lastIndexOf(".") + 1);
				//过滤jre
				if(Regexp.isExist("/jre[.\\-\\w]*/lib/", lower)){
					continue;
				}
				//过滤开发工具集成的固定jar包
				if(jarFilterList.contains(filename.toLowerCase())){
					continue;
				}
				//取jar包且未加载过
				if("jar".equals(ext)){
					//目标文件
					File target = new File(targetLib, filename);
					//不存在，则复制
					if(!target.exists()){
						//复制文件
						FileUtils.copy(file, target);
					}
					//判断文件名
					if(filename.matches("myframe-utils.*\\.jar")) {
						corePacks[0] = target;
					}else if(filename.matches("myframe-core.*\\.jar")) {
						corePacks[1] = target;
					}
				}
			}//依赖项目
			else{
				//打包名称
				String packname = null;
				// /bin结尾
				if(lower.endsWith("/bin")){
					packname = file.getParentFile().getName();
				}// /build/classes结尾
				else if(lower.endsWith("/build/classes")){
					packname = file.getParentFile().getParentFile().getName();
				}//其它，如：/out/production/myframe-utils，直接取目录名
				else{
					packname = file.getName();
				}
				//跳过当前项目
				if(projectName.equalsIgnoreCase(packname)){
					continue;
				}
				try {
					//目标文件
					File target = new File(targetLib, packname + ".jar");
					//打压缩包
					FileUtils.compressChildrenToZip(file.getAbsolutePath(), target.getParent(), target.getName());
					//判断文件名
					if("myframe-utils".equalsIgnoreCase(packname)) {
						corePacks[0] = target;
					}else if("myframe-core".equalsIgnoreCase(packname)) {
						corePacks[1] = target;
					}
				}catch(IOException e){
					throw new RuntimeException("项目打包失败：" + file.getAbsolutePath(), e);
				}
			}
		}
		//返回
		return corePacks;
	}
	
	/**
	 * 获取MANIFEST.MF文件内容
	 * @param mainClass 运行主类
	 */
	private static String getManifestInfo(String mainClass) {
		//容器
		StringBuilder sb = new StringBuilder();
		//Manifest版本
		sb.append("Manifest-Version: 1.0").append("\r\n");
		//生成环境
		sb.append("Created-By: " + System.getProperty("java.version") + " (" + System.getProperty("java.vendor") + ")").append("\r\n");
		//主类（加载器）
		sb.append("Main-Class: ").append(loaderClass).append("\r\n");
		//真正主类
		sb.append("Real-Main-Class: " + mainClass).append("\r\n");
		/** 生成ClassPath（结束） **/
		//末尾添加两个空行
		sb.append("\r\n");
		//返回
		return sb.toString();
	}
	
	/**
	 * 打印日志
	 * @param message 日志消息
	 */
	private static void printLog(String message) {
		//调用路径
		StackTraceElement trace = Thread.currentThread().getStackTrace()[2];
		//日志前缀
		String prefix = sdf.format(new Date()) + " [" + trace.getClassName() + "#" + trace.getLineNumber() + "] ";
		//打印
		System.out.println(prefix + message);
	}
}
